'use strict'

const { promisify } = require('util')

const common = require('electron-installer-common')
const fs = require('fs-extra')
const fsize = promisify(require('get-folder-size'))
const parseAuthor = require('parse-author')
const path = require('path')
const wrap = require('word-wrap')

const spawn = require('./spawn')
const tmp = require('tmp-promise')

const defaultRename = (dest, src) => {
  return path.join(dest, '<%= name %>_<%= version %><% if (revision) { %>-<%= revision %><% } %>_<%= arch %>.deb')
}

/**
 * Transforms a SemVer version into a Debian-style version.
 *
 * Use '~' on pre-releases for proper Debian version ordering.
 * See https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
 */
function transformVersion (version) {
  return version.replace(/(\d)[_.+-]?((RC|rc|pre|dev|beta|alpha)[_.+-]?\d*)$/, '$1~$2')
}

class DebianInstaller extends common.ElectronInstaller {
  get contentFunctions () {
    return [
      'copyApplication',
      'copyLinuxIcons',
      'copyScripts',
      // 'createBinarySymlink',
      'createControl',
      // 'createCopyright',
      'createDesktopFile',
      'createInfoJson'
      // 'createOverrides'
    ]
  }

  get baseAppDir () {
    return 'opt'
  }

  get baseFileDir () {
    return path.join(this.stagingDir, this.baseAppDir, 'apps', this.appIdentifier)
  }

  get stagingAppDir () {
    return path.join(this.baseFileDir, 'files')
  }

  get defaultDesktopTemplatePath () {
    return path.resolve(__dirname, '../resources/desktop.ejs')
  }

  get packagePattern () {
    return path.join(this.stagingDir, '../*.deb')
  }

  /**
   * Copy the application into the package.
   */
  async copyApplication () {
    await super.copyApplication(src => src !== path.join(this.options.src, 'LICENSE'))
    return this.updateSandboxHelperPermissions()
  }

  async createCopyright () {
    const copyrightFile = path.join(this.baseFileDir, 'entries', 'doc', this.appIdentifier, 'copyright')
    console.log(`Creating copyright file at ${copyrightFile}`)
    return common.wrapError('creating copyright file', async () => {
      await fs.ensureDir(path.dirname(copyrightFile), '0755')
      await this.copyLicense(copyrightFile)
      await fs.chmod(copyrightFile, '0644')
    })
  }

  async createDesktopFile () {
    const templatePath = this.options.desktopTemplate || this.defaultDesktopTemplatePath
    const baseDir = path.join(this.baseFileDir, 'entries', 'applications')
    if (!this.options.startFilePath) {
      this.options.startFilePath = path.resolve('/opt', 'apps', this.appIdentifier, 'files', this.options.bin)
    }
    if (!this.options.desktopIcon) {
      this.options.desktopIcon = this.options.name
    }
    await common.createDesktopFile(templatePath, baseDir, this.appIdentifier, this.options)
    return common.createDesktopFile(templatePath, path.join(this.stagingDir, 'usr', 'share', 'applications'), this.appIdentifier, this.options)
  }

  async createInfoJson () {
    const info = {
      appid: this.options.name,
      name: this.options.bin,
      version: this.options.version,
      arch: [this.options.arch],
      permissions: {
        autostart: true,
        notification: false,
        trayicon: true,
        clipboard: false,
        account: false,
        bluetooth: false,
        camera: false,
        audio_record: false,
        installed_apps: false
      }
    }
    await fs.outputFile(path.join(this.baseFileDir, 'info'), JSON.stringify(info, null, 4))
  }

  async copyHicolorIcons () {
    return Promise.all(Object.keys(this.options.icon).map(resolution => {
      const iconSrc = this.options.icon[resolution]
      const iconExt = ['scalable', 'symbolic'].includes(resolution) ? 'svg' : 'png'
      const iconName = resolution === 'symbolic' ? `${this.appIdentifier}-symbolic` : this.appIdentifier
      const iconFile = path.join(this.baseFileDir, 'entries', 'icons', 'hicolor', resolution, 'apps', `${iconName}.${iconExt}`)

      return common.wrapError('creating hicolor icon file', async () => this.copyIcon(iconSrc, iconFile))
    }))
  }

  /**
   * Copy debian scripts.
   */
  copyScripts () {
    const scriptNames = ['preinst', 'postinst', 'prerm', 'postrm']

    return common.wrapError('creating script files', async () =>
      Promise.all(Object.keys(this.options.scripts).map(async (key) => {
        const item = this.options.scripts[key]
        if (scriptNames.includes(key)) {
          const scriptFile = path.join(this.stagingDir, 'DEBIAN', key)
          this.options.logger(`Creating script file at ${scriptFile}`)

          await fs.copy(item, scriptFile)
          return fs.chmod(scriptFile, 0o755)
        } else {
          throw new Error(`Wrong executable script name: ${key}`)
        }
      }))
    )
  }

  /**
   * Creates the control file for the package.
   *
   * See: https://www.debian.org/doc/debian-policy/ch-controlfields.html
   */
  createControl () {
    const src = path.resolve(__dirname, '../resources/control.ejs')
    const dest = path.join(this.stagingDir, 'DEBIAN', 'control')
    this.options.logger(`Creating control file at ${dest}`)

    return common.wrapError('creating control file', async () => this.createTemplatedFile(src, dest))
  }

  /**
   * Package everything using `dpkg` and `fakeroot`.
   */
  async createPackage () {
    this.options.logger(`Creating package at ${this.stagingDir}`)

    const command = ['--build', this.stagingDir]
    if (process.platform === 'darwin') {
      command.unshift('--root-owner-group')
    }
    command.unshift('dpkg-deb')

    const output = await spawn('fakeroot', command, this.options.logger)
    this.options.logger(`dpkg-deb output: ${output}`)
  }

  /**
   * Get the hash of default options for the installer. Some come from the info
   * read from `package.json`, and some are hardcoded.
   */
  async generateDefaults () {
    const pkg = this.userSupplied.pkg || {}
    const size = await fsize(this.userSupplied.src)

    this.defaults = Object.assign(common.getDefaultsFromPackageJSON(pkg), {
      version: transformVersion(pkg.version || '0.0.0'),

      section: 'utils',
      priority: 'optional',
      size: Math.ceil((size || 0) / 1024),

      maintainer: this.getMaintainer(pkg.author),

      icon: path.resolve(__dirname, '../resources/icon.png'),
      lintianOverrides: []
    })

    return this.defaults
  }

  /**
   * Flattens and merges default values, CLI-supplied options, and API-supplied options.
   */
  generateOptions () {
    super.generateOptions()

    this.options.name = this.sanitizeName(this.options.name)

    if (!this.options.description && !this.options.productDescription) {
      throw new Error("No Description or ProductDescription provided. Please set either a description in the app's package.json or provide it in the this.options.")
    }

    if (this.options.description) {
      this.options.description = this.normalizeDescription(this.options.description)
    }

    if (this.options.productDescription) {
      this.options.productDescription = this.normalizeExtendedDescription(this.options.productDescription)
    }

    // Create array with unique values from default & user-supplied dependencies
    for (const prop of ['depends', 'recommends', 'suggests', 'enhances', 'preDepends']) {
      this.options[prop] = common.mergeUserSpecified(this.userSupplied, prop, this.defaults)
    }
    return this.options
  }

  /**
   * Generates a Debian-compliant maintainer value from a package.json author field.
   */
  getMaintainer (author) {
    if (author) {
      if (typeof author === 'string') {
        author = parseAuthor(author)
      }
      const maintainer = [author.name]
      if (author.email) {
        maintainer.push(`<${author.email}>`)
      }

      return maintainer.join(' ')
    }
  }

  /**
   * Normalize the description by replacing all newlines in the description with spaces, since it's
   * supposed to be one line.
   */
  normalizeDescription (description) {
    return description.replace(/[\r\n]+/g, ' ')
  }

  /**
   * Ensure blank lines have the "." that denotes a blank line in the control file. Wrap any
   * extended description lines to avoid lintian warnings about
   * `extended-description-line-too-long`.
   */
  normalizeExtendedDescription (extendedDescription) {
    return extendedDescription
      .replace(/\r\n/g, '\n') // Fixes errors when finding blank lines in Windows
      .replace(/^$/mg, '.')
      .split('\n')
      .map(line => wrap(line, { width: 80, indent: ' ' }))
      .join('\n')
  }

  /**
   * Sanitize package name per Debian docs:
   * https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-source
   */
  sanitizeName (name) {
    const sanitized = common.sanitizeName(name.toLowerCase(), '-+.a-z0-9')
    if (sanitized.length < 2) {
      throw new Error('Package name must be at least two characters')
    }
    if (/^[^a-z0-9]/.test(sanitized)) {
      throw new Error('Package name must start with an ASCII number or letter')
    }

    return sanitized
  }

  async createStagingDir () {
    console.log('Creating staging directory')

    return common.wrapError('creating staging directory', async () => {
      const dir = await tmp.dir({ prefix: 'deb-builder-', unsafeCleanup: true })
      this.stagingDir = path.join(dir.path, `${this.appIdentifier}_${this.options.version}_${this.options.arch}`)
      return fs.ensureDir(this.stagingDir, '0755')
    })
  }
}

/* ************************************************************************** */

module.exports = async data => {
  data.rename = data.rename || defaultRename
  data.logger = console.log

  if (process.umask() !== 0o0022 && process.umask() !== 0o0002) {
    console.warn(`The current umask, ${process.umask().toString(8)}, is not supported. You should use 0022 or 0002`)
  }

  const installer = new DebianInstaller(data)

  console.log('开始生成默认配置')
  await installer.generateDefaults()
  console.log('开始生成配置')
  await installer.generateOptions()
  console.log('开始创建基础目录')
  await installer.createStagingDir()
  console.log('开始构建打包内容')
  await installer.createContents()
  console.log('开始打包')
  await installer.createPackage()
  console.log('生成安装包')
  await installer.movePackage()
  // 返回安装包路径
  return installer.options.packagePaths[0]
}

module.exports.Installer = DebianInstaller
module.exports.transformVersion = transformVersion
